package utils;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;

public class DateUtil {
    static long _timestampOffset = 0;
    private static final long ONE_SECOND = 1000L;
    private static final long ONE_MINUTE = 60 * ONE_SECOND;
    private static final long ONE_HOUR = 60 * ONE_MINUTE;

    public static final int MILLISECONDS_IN_MINUTE = 1000 * 60;

    private static final DateFormat ISO_DATE_HOUR_MINUTE_SECONDS = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
    private static final DateFormat ISO_DATE_HOUR_MINUTE = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm");
    private static final DateFormat ISO_DATE = new SimpleDateFormat("yyyy-MM-dd");

    private static final String[] MONTHS_IN_ENGLISH =
        {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"};

    /**
     * Get the day names based on the mapped indices (0-6)
     */
    private static final String[] DAY_OF_WEEK_NAMES;
    /**
     * This defins the max index for the days of the week so we don't have to assume
     * anything about their values. Delcare an array [MAX_DAY_INDEX + 1] and you
     * can access that array using Calendar.MONDAY, etc. This might not be a dense
     * array. There could be unused holes.
     */
    public static final int MAX_DAY_INDEX;

    private static final Map<String, String> _cfToJavaFormat = new HashMap<String, String>();

    static {
        _cfToJavaFormat.put("mm/dd/yy", "MM/dd/yy");
        _cfToJavaFormat.put("dd/mm/yy", "dd/MM/yy");
        _cfToJavaFormat.put("yy/mm/dd", "yy/MM/dd");
        _cfToJavaFormat.put("h:mmtt", "h:mma");
        _cfToJavaFormat.put("HH:mm", "HH:mm");
    }

    static {
        int maxDayIndex = Math.max(Calendar.SUNDAY, Calendar.MONDAY);
        maxDayIndex = Math.max(maxDayIndex, Calendar.TUESDAY);
        maxDayIndex = Math.max(maxDayIndex, Calendar.WEDNESDAY);
        maxDayIndex = Math.max(maxDayIndex, Calendar.THURSDAY);
        maxDayIndex = Math.max(maxDayIndex, Calendar.FRIDAY);
        maxDayIndex = Math.max(maxDayIndex, Calendar.SATURDAY);
        MAX_DAY_INDEX = maxDayIndex;

        DAY_OF_WEEK_NAMES = new String[8];
        DAY_OF_WEEK_NAMES[0] = "JUNK";
        DAY_OF_WEEK_NAMES[Calendar.SUNDAY] = "Sunday";
        DAY_OF_WEEK_NAMES[Calendar.MONDAY] = "Monday";
        DAY_OF_WEEK_NAMES[Calendar.TUESDAY] = "Tuesday";
        DAY_OF_WEEK_NAMES[Calendar.WEDNESDAY] = "Wednesday";
        DAY_OF_WEEK_NAMES[Calendar.THURSDAY] = "Thursday";
        DAY_OF_WEEK_NAMES[Calendar.FRIDAY] = "Friday";
        DAY_OF_WEEK_NAMES[Calendar.SATURDAY] = "Saturday";
    }

    private static final DateFormat[] GUESS_TIME_FORMATS =
        {new SimpleDateFormat("hh:mm:ss a"), new SimpleDateFormat("hh:mm:ssa"), new SimpleDateFormat("HH:mm:ss"), new SimpleDateFormat("hh:mm a"),
            new SimpleDateFormat("hh:mma"), new SimpleDateFormat("HH:mm"), new SimpleDateFormat("hh a"), new SimpleDateFormat("hha"),
            new SimpleDateFormat("H")};
    /**
     * Singleton instance.  Most Java code will simply call this class' static methods.
     * A singleton is available for use on the ColdFusion tier (via WebUtil.getDateUtil()) to avoid
     * constructing an instance of this class on every use.
     */
    private static final DateUtil INSTANCE = new DateUtil();
    private static final DateFormat[] CF_DATE_FORMATS =
        {new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"), new SimpleDateFormat("yyyy-MM-dd HH:mm"), new SimpleDateFormat("yyyy-MM-dd")};

    /**
     * Singleton private constructor.  Most Java code will simply call this class' static methods.
     * A singleton is available for use on the ColdFusion tier (via WebUtil.getDateUtil()) to avoid
     * constructing an instance of this class on every use.
     */
    private DateUtil() {
    }

    /**
     * Singleton getter.  Most Java code will simply call this class' static methods.
     * A singleton is available for use on the ColdFusion tier (via WebUtil.getDateUtil()) to avoid
     * constructing an instance of this class on every use.
     *
     * @return the singleton
     */
    public static DateUtil getInstance() {
        return INSTANCE;
    }

    public static String getMonthInEnglish(int monthBaseZero) {
        return MONTHS_IN_ENGLISH[monthBaseZero];
    }

    /**
     * Return the age years based on the date of birth
     */
    public static int getAgeInYears(Date dob) {
        return getAgeInYears(dob, DateUtil.dateNow());
    }

    public static boolean isBetweenInclusive(Date theDate, Date start, Date end) {
        return !(theDate.before(start) || theDate.after(end));
    }

    public static int getAgeInYears(Date dob, Date today) {
        Calendar dobCal = Calendar.getInstance();
        dobCal.setTime(dob);

        Calendar todayCal = Calendar.getInstance();
        todayCal.setTime(today);

        Calendar lastBirthday = Calendar.getInstance();
        lastBirthday.setTime(todayCal.getTime());
        lastBirthday.set(Calendar.MONTH, dobCal.get(Calendar.MONTH));
        lastBirthday.set(Calendar.DAY_OF_MONTH, dobCal.get(Calendar.DAY_OF_MONTH));
        if (todayCal.before(lastBirthday)) {
            lastBirthday.add(Calendar.YEAR, -1);
        }

        return lastBirthday.get(Calendar.YEAR) - dobCal.get(Calendar.YEAR);
    }

    /**
     * Determine if the dayOfWeekStay is in the date range
     */
    public static boolean hasDayOfWeekStay(Date beginDate, Date endDate, int dayOfWeekStay) {
        // Get the day of week
        GregorianCalendar begin = new GregorianCalendar();
        begin.setTime(beginDate);
        int dayOfWeekBegin = begin.get(Calendar.DAY_OF_WEEK);
        // Number of days from today to the dayOfWeekStay
        int dayCount = dayOfWeekStay - dayOfWeekBegin;
        dayCount = (dayCount < 0) ? (dayCount + 7) : dayCount;
        // Number of days in booking
        int daysBetween = daysBetween(beginDate, endDate);
        // Number of days in booking is greater than the number of days to the dayOfWeekStay
        return daysBetween > dayCount;
    }

    public static Date getEarliestDate(Date date1, Date date2) {
        if (date1 == null) {
            return date2;
        }
        if (date2 == null) {
            return date1;
        }
        return date1.before(date2) ? date1 : date2;
    }

    public static Date getLatestDate(Date date1, Date date2) {
        if (date1 == null) {
            return date2;
        }
        if (date2 == null) {
            return date1;
        }
        return date1.after(date2) ? date1 : date2;
    }

    public static Date addDays(Date date, int days) {
        if (days == 0) {
            return date;
        }
        GregorianCalendar result = new GregorianCalendar();
        result.setTime(date);
        result.add(Calendar.DAY_OF_YEAR, days);
        return result.getTime();
    }

    public static Date addHours(Date date, int hours) {
        if (hours == 0) {
            return date;
        }
        GregorianCalendar result = new GregorianCalendar();
        result.setTime(date);
        result.add(Calendar.HOUR, hours);
        return result.getTime();
    }

    public static Date addMinutes(Date date, int minutes) {
        if (minutes == 0) {
            return date;
        }
        GregorianCalendar result = new GregorianCalendar();
        result.setTime(date);
        result.add(Calendar.MINUTE, minutes);
        return result.getTime();
    }

    public static Date addMilliseconds(Date date, int milliseconds) {
        if (milliseconds == 0) {
            return date;
        }
        GregorianCalendar result = new GregorianCalendar();
        result.setTime(date);
        result.add(Calendar.MILLISECOND, milliseconds);
        return result.getTime();
    }

    public static Date addMonths(Date date, int months) {
        if (months == 0) {
            return date;
        }
        GregorianCalendar result = new GregorianCalendar();
        result.setTime(date);
        result.add(Calendar.MONTH, months);
        return result.getTime();
    }

    public static Date addYears(Date date, int years) {
        GregorianCalendar result = new GregorianCalendar();
        result.setTime(date);
        result.add(Calendar.YEAR, years);
        return result.getTime();
    }

    public static Calendar calendarNow() {
        Calendar result = new GregorianCalendar();
        Date date = result.getTime();
        date.setTime(dateNowInMillisecondsSince1970());
        result.setTime(date);
        return result;
    }

    public static Date dateNow() {
        Date date = new Date();
        date.setTime(dateNowInMillisecondsSince1970());
        return date;
    }

    public static long dateNowInMillisecondsSince1970() {
        return new Date().getTime() - _timestampOffset;
    }

    /**
     * Return the number of nights a hotel would charge for. This is the same as
     * daysBetween, except that the result cannot be 0. If it is, is in changed
     * to 1.
     */
    public static int daysBetweenOrOne(Date pickup, Date dropoff) {
        return Math.max(daysBetween(pickup, dropoff), 1);
    }

    /**
     * Return true if the day is between two days (inclusive)
     */
    public static boolean isDayInRange(Date day, Date begin, Date end) {
        return sameDay(day, begin) || sameDay(day, end) || (day.after(begin) && day.before(end));
    }

    /**
     * Creaet a date using the date protion of one Date and the time portion
     * of the other.
     */
    public static Date combineDateAndTime(Date date, Date time) {
        if (date == null) {
            return time;
        } else if (time == null) {
            // clear out any residual time portion to clarify time is not known
            return getEarliestTimeInDay(date);
        }
        GregorianCalendar result = new GregorianCalendar();
        result.setTime(time);
        GregorianCalendar dateCalendar = new GregorianCalendar();
        dateCalendar.setTime(date);
        result.set(Calendar.YEAR, dateCalendar.get(Calendar.YEAR));
        result.set(Calendar.MONTH, dateCalendar.get(Calendar.MONTH));
        result.set(Calendar.DATE, dateCalendar.get(Calendar.DATE));
        return result.getTime();
    }

    /**
     * Extract the time from a date/time Date
     */
    public static Date extractTime(Date datetime) {
        if (datetime == null) {
            return null;
        }

        GregorianCalendar result = new GregorianCalendar();
        result.setTime(datetime);
        result.set(Calendar.YEAR, 0);
        result.set(Calendar.MONTH, 0);
        result.set(Calendar.DATE, 0);
        return result.getTime();
    }

    public static Date convertDefaultTimeZone(Date timestamp, TimeZone targetTimeZone) {
        return convertTimeZone(timestamp, TimeZone.getDefault(), targetTimeZone);
    }

    /**
     * Convert the date so that it will display as if it was in GMT. Note that all Dates in java are stored in GMT, so this routine
     * really changes things so that if it is displayed using the default time zone it will have GMT values.
     */
    public static Date convertToGmt(Date timestamp) {
        return convertTimeZone(timestamp, TimeZone.getTimeZone("GMT"), TimeZone.getDefault());
    }

    /**
     * Convert a UTC date that was parsed with the wrong time zone. This routine assumes that the original
     * date was parsed relative to fromZone and should have been parsed relative to toZone. Note that
     * dates are always stored in java in GMT.
     */
    public static Date convertTimeZone(Date utcTime, TimeZone fromZone, TimeZone toZone) {
        GregorianCalendar current = new GregorianCalendar(fromZone);
        current.setTime(utcTime);
        GregorianCalendar result = new GregorianCalendar(toZone);
        result.set(current.get(Calendar.YEAR), current.get(Calendar.MONTH), current.get(Calendar.DATE), current.get(Calendar.HOUR_OF_DAY),
            current.get(Calendar.MINUTE), current.get(Calendar.SECOND));
        return result.getTime();
    }

    /**
     * Return the days between the two specified dates. Extra hours and minutes
     * are truncated. If both arguments are in the same day, the result is 0
     * even if the times are different.
     * <p/>
     * If you want to calculate the numger of nights in a hotel stay, use
     * daysBetweenOrOne, since it makes sure the result is at least 1.
     */
    public static int daysBetween(Date start, Date end) {
        start = getEarliestTimeInDay(start);
        end = getEarliestTimeInDay(end);
        // Clean up negative 0/1 hour
        long duration = Math.abs(end.getTime() - start.getTime());
        // Add one hour in case the duration includes a
        // 23 hour Daylight Savings spring forward day.
        long nDays = (duration + ONE_HOUR) / (24 * ONE_HOUR);

        return (int) nDays;
    }

    /**
     * Provides begin date of the given month
     *
     * @param date
     * @param month
     * @return Date
     */
    @SuppressWarnings({"UnusedDeclaration"})
    // used from CF
    public static Date beginDateOfGivenMonth(Date date, int month) {
        GregorianCalendar result = new GregorianCalendar();
        // set the given date
        result.setTime(date);
        // set the month required
        result.add(Calendar.MONTH, month);
        // set the day to the begin of the month
        result.set(Calendar.DAY_OF_MONTH, result.getActualMinimum(Calendar.DAY_OF_MONTH));
        // set the begin time of the day which is 00:00:00
        result.setTime(getEarliestTimeInDay(result.getTime()));
        return result.getTime();
    }

    /**
     * Provides end date of the given month
     *
     * @param date
     * @param month
     * @return Date
     */
    @SuppressWarnings({"UnusedDeclaration"})
    // used from CF
    public static Date endDateOfGivenMonth(Date date, int month) {
        GregorianCalendar result = new GregorianCalendar();
        // set the given date
        result.setTime(date);
        // set the month required
        result.add(Calendar.MONTH, month);
        // set the day to the end of the month
        result.set(Calendar.DAY_OF_MONTH, result.getActualMaximum(Calendar.DAY_OF_MONTH));
        // set the begin time of the day which is 23:59:59
        result.setTime(getLatestTimeInDay(result.getTime()));
        return result.getTime();
    }

    /**
     * Convenience method for default time zone.
     */
    public static String formatDate(Date date, String format) {
        return formatDate(date, format, null);
    }

    /**
     * This function formats the date into a string using the specified
     * format. For rules in creating the format string see the documentation
     * for java.text.SimpleDateFormatter. The time zone may be null, in which
     * case the default time zone is used.
     */
    public static String formatDate(Date date, String format, TimeZone zone) {
        if (date == null) {
            return null;
        }
        SimpleDateFormat formatter = new SimpleDateFormat(format);
        if (zone != null) {
            formatter.setTimeZone(zone);
        }
        return formatter.format(date);
    }

    /**
     * Return a date string for coldfusion in the format "yyyy-MM-dd hh:mm:ss"
     */
    public static String formatDateCf(Date date) {
        return formatDate(date, "yyyy-MM-dd HH:mm:ss");
    }

    public static String formatDateCf(Date date, TimeZone timeZone) {
        return formatDate(date, "yyyy-MM-dd HH:mm:ss", timeZone);
    }

    /**
     * Format the specified date in ISO format (yyyy-MM-dd). The time portion
     * of the object is ignored.
     */
    public static String formatIsoDate(Date date) {
        return formatDate(date, "yyyy-MM-dd");
    }

    public static String formatIsoTimestamp(Date date) {
        return formatIsoTimestamp(date, false);// no seconds
    }

    public static String formatIsoTimestamp(Date date, String timeZone) {
        return formatIsoTimestamp(date, TimeZone.getTimeZone(timeZone), true);
    }

    public static String formatIsoTimestamp(Date date, TimeZone timeZone) {
        return formatIsoTimestamp(date, timeZone, false);// no seconds
    }

    public static String formatIsoTimestamp(Date date, boolean seconds) {
        return formatIsoTimestamp(date, null, seconds);
    }

    public static String formatIsoTimestamp(Date date, TimeZone timeZone, boolean seconds) {
        if (seconds) {
            return formatDate(date, "yyyy-MM-dd'T'HH:mm:ss", timeZone);
        } else {
            return formatDate(date, "yyyy-MM-dd'T'HH:mm", timeZone);
        }
    }

    /**
     * Format Date to ISO timestamp String with millisecond precision.
     *
     * @param date
     * @return String - Date formatted to ISO timestamp with millisecond precision.
     */
    public static String formatIsoTimestampWithMillis(Date date) {
        return formatDate(date, "yyyy-MM-dd'T'HH:mm:ss:SSSS");
    }

    /*
     * Day of week with Sun = 1
     */

    public static int getDayOfWeek(Date date) {
        GregorianCalendar calendar = new GregorianCalendar();
        calendar.setTime(date);
        return calendar.get(GregorianCalendar.DAY_OF_WEEK);
    }

    public static int getDayOfMonth(Date date) {
        GregorianCalendar calendar = new GregorianCalendar();
        calendar.setTime(date);
        return calendar.get(GregorianCalendar.DAY_OF_MONTH);
    }

    public static int getMonthZeroBaseIndex(Date date) {
        GregorianCalendar calendar = new GregorianCalendar();
        calendar.setTime(date);
        return calendar.get(GregorianCalendar.MONTH);
    }

    public static int getMonth(Date date) {
        return getMonthZeroBaseIndex(date) + 1;
    }

    public static int getYear(Date date) {
        GregorianCalendar calendar = new GregorianCalendar();
        calendar.setTime(date);
        return calendar.get(GregorianCalendar.YEAR);
    }

    public static int getHourOfDay(Date date) {
        GregorianCalendar calendar = new GregorianCalendar();
        calendar.setTime(date);
        return calendar.get(GregorianCalendar.HOUR_OF_DAY);
    }

    public static int getMinute(Date date) {
        GregorianCalendar calendar = new GregorianCalendar();
        calendar.setTime(date);
        return calendar.get(GregorianCalendar.MINUTE);
    }

    public static String getDayOfWeekName(int day) {
        return DAY_OF_WEEK_NAMES[day];
    }

    /**
     * Return a date object with the earliest time in the day (00:00:00.000)
     */
    public static Date getEarliestTimeInDay(Date date) {
        GregorianCalendar calendar = new GregorianCalendar();
        calendar.setTime(date);
        calendar.set(GregorianCalendar.HOUR_OF_DAY, 0);
        calendar.set(GregorianCalendar.MINUTE, 0);
        calendar.set(GregorianCalendar.SECOND, 0);
        calendar.set(GregorianCalendar.MILLISECOND, 0);
        return calendar.getTime();
    }

    /**
     * Return a date object with the latest time in the day (23:59:59.999)
     */
    public static Date getLatestTimeInDay(Date date) {
        GregorianCalendar calendar = new GregorianCalendar();
        calendar.setTime(date);
        calendar.set(GregorianCalendar.HOUR_OF_DAY, 23);
        calendar.set(GregorianCalendar.MINUTE, 59);
        calendar.set(GregorianCalendar.SECOND, 59);
        calendar.set(GregorianCalendar.MILLISECOND, 999);
        return calendar.getTime();
    }

    public static int hotelNights(Date checkin, Date checkout) {
        return daysBetweenOrOne(checkin, checkout);
    }

    public static int hoursBetween(Date start, Date end) {
        return (int) ((end.getTime() - start.getTime()) / ONE_HOUR);
    }

    public static int minutesBetween(Date start, Date end) {
        return (int) ((end.getTime() - start.getTime()) / ONE_MINUTE);
    }

    public static Date nextDay(Date date) {
        return addDays(date, 1);
    }

    public static synchronized Date parseDate(String dateString, DateFormat format) {
        try {
            return format.parse(dateString);
        } catch (ParseException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public static Date parseDate(String dateString, String formatString) {
        Locale locale = new Locale("es", "MX");
        SimpleDateFormat format = new SimpleDateFormat(formatString, locale);
        try {
            return format.parse(dateString);
        } catch (ParseException e) {
            throw new IllegalArgumentException(e);
        }
    }

    private static synchronized Date parseDateTryingMultipleFormats(String dateString, DateFormat... formats) {
        for (DateFormat format : formats) {
            try {
                return format.parse(dateString);
            } catch (ParseException e) {
                // ignored
            }
        }
        throw new IllegalArgumentException("Could not parse the string " + dateString + " into a Date object");
    }

    /**
     * Parse a time.  Guess the format.
     *
     * @param timeString the string to be parsed
     * @return a Date object containing the parsed time
     * @ if none of the GUESS_TIME_FORMATS works
     */
    public static Date parseTimeGuessFormat(String timeString) {
        return parseDateTryingMultipleFormats(timeString, GUESS_TIME_FORMATS);
    }

    public static Date parseIsoDate(String dateString, Date defaultValue) {
        try {
            return parseIsoDate(dateString);
        } catch (Exception e) {
            return defaultValue;
        }
    }

    /**
     * Parse the specified date string and return a Date object. Return null
     * if the string does not match ISO format.
     * <p/>
     * This routine accepts and aprses string in either of the following
     * formats: yyyy-MM-ddThh:mm or yyyy-MM-dd.
     */
    public static Date parseIsoDate(String dateString) {
        return parseDateTryingMultipleFormats(dateString, ISO_DATE_HOUR_MINUTE_SECONDS, ISO_DATE_HOUR_MINUTE, ISO_DATE);
    }

    /**
     * Return an boolean indicating if the two timestamps specified are on
     * the same day.
     */
    public static boolean sameDay(Date timestamp1, Date timestamp2) {
        if ((timestamp1 == null) || (timestamp2 == null)) {
            return false;
        }
        GregorianCalendar cal1 = new GregorianCalendar();
        cal1.setTime(timestamp1);
        GregorianCalendar cal2 = new GregorianCalendar();
        cal2.setTime(timestamp2);
        return (cal1.get(GregorianCalendar.YEAR) == cal2.get(GregorianCalendar.YEAR)) &&
            (cal1.get(GregorianCalendar.DAY_OF_YEAR) == cal2.get(GregorianCalendar.DAY_OF_YEAR));
    }

    public static void setTimestampForDebugging(String date) {
        setTimestampForDebugging(parseIsoDate(date));
    }

    public static void setTimestampForDebugging(Date date) {
        _timestampOffset = new Date().getTime() - date.getTime();
    }

    public static void clearDebuggingTimestampOffset() {
        _timestampOffset = 0;
    }

    /**
     * This a convenience routine to convert a date with an assumed time of 0000.
     */
    public static Date setYearAccordingToSlidingWindow(String dateMonth) {
        return setYearAccordingToSlidingWindow(dateMonth, "0000");
    }

    /**
     * Many CRS's give a month and day, assuming that the year is obvious.
     * This routine updates the parsed date by setting the year so that the
     * date is after the current date in the next 11 months, or prior to
     * this date in the last month. The month format is ddMMM (e.g., 13JAN)
     * and the hourMinute format is HHmm (e.g., 1503).
     */
    public static Date setYearAccordingToSlidingWindow(String dateMonth, String hourMinute) {
        if (hourMinute.length() < 4) {
            hourMinute = "0" + hourMinute;// Add leading 0
        }
        Date date = parseDate(dateMonth + hourMinute, "ddMMMHHmm");
        Calendar now = calendarNow();
        Calendar result = calendarNow();
        result.setTime(date);
        Calendar day33Past = calendarNow();
        day33Past.add(Calendar.DAY_OF_YEAR, -33);
        Calendar day332Hence = calendarNow();
        day332Hence.add(Calendar.DAY_OF_YEAR, 332);
        if (result.before(day33Past) || result.after(day332Hence)) {
            boolean leapDay = dateMonth.toUpperCase().equals("29FEB");
            result.set(Calendar.YEAR, now.get(Calendar.YEAR));
            while (result.before(day33Past)) {
                result.add(Calendar.YEAR, 1);
            }
            if (leapDay) {
                result.set(Calendar.MONTH, Calendar.FEBRUARY);
                result.set(Calendar.DAY_OF_MONTH, 29);
            }
        }
        return result.getTime();
    }

    public static Date min(Date date1, Date date2) {
        if (date1.before(date2)) {
            return date1;
        } else {
            return date2;
        }
    }

    public static Date max(Date date1, Date date2) {
        if (date1.before(date2)) {
            return date2;
        } else {
            return date1;
        }
    }
}
